package de.gameduell.net.src.communication {
	import de.gameduell.net.src.connection.Synchronisation;	

	import flash.events.DataEvent;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.events.TimerEvent;
	import flash.system.Security;
	import flash.utils.Timer;
	
	import de.gameduell.framework.debug.Debug;
	import de.gameduell.framework.util.FlashVars;
	import de.gameduell.framework.util.LockManager;
	import de.gameduell.net.src.communication.CommunicationEvent;
	import de.gameduell.net.src.communication.SyncEvent;
	import de.gameduell.net.src.connection.IConnection;
	import de.gameduell.net.src.connection.PollConnection;
	import de.gameduell.net.src.connection.SocketConnection;
	import de.gameduell.net.src.message.IMessage;
	import de.gameduell.net.src.message.MessageEvent;
	import de.gameduell.net.src.protocol.CommunicationRequests;
	import de.gameduell.net.src.protocol.IProtocol;
	import de.gameduell.net.src.protocol.Protocol_V2;	

	public class Communication extends BaseCommunication implements ICommunication {

		public const PROTOCOL_V2:IProtocol = new Protocol_V2();

		public static const DEBUG_ENABLED:Boolean = false;

		public static const SOCKET_CONNECTION:String   = "socket";
		public static const LOADVARS_CONNECTION:String = "loadvars";
		
		private const RECONNECT_TIMER:int = 8000;
		private const MAX_RECONNECTS:int  = 5;
		private const PORT:int 	          = 8042;

		private var connection:IConnection;
		private var confirmationQueue:Array;
		private var lockManager:LockManager;
		private var reconnectTimer:Timer;
		private var reconnectCount:int;
		private var host:String;
		private var connectionClosed:Boolean  = false;
		
		public function Communication(){
			super("Communication");
			host = FlashVars.getKeyValue("gchost");
			reconnectCount = 0;
			
			messageQueue = new MessageQueue();		
			messageQueue.addEventListener(MessageEvent.MESSAGE_EXPIRED, onMessageExpired);
			messageQueue.addEventListener(MessageEvent.MAX_RETRIES_REACHED, disconnect);
			messageQueue.addEventListener(MessageEvent.NEXT_MESSAGE, onNextMessage);
			addEventListener(CommunicationEvent.DISCONNECT, disconnect);
			
			// messages sended to the server which have to be acked
			confirmationQueue = new Array();
			
			reconnectTimer = new Timer(RECONNECT_TIMER);
			reconnectTimer.addEventListener(TimerEvent.TIMER, onReconnectTimer);
		
			lockManager = LockManager.getInstance();
		}

		public function connect():void{
			Security.loadPolicyFile("xmlsocket://" + host + ":" + PORT);
			connection = new SocketConnection(this,settings.socketSync);
			log("connect: connecting to host: " + host, Debug.CONNECTION);
			registerConnectionEvents();
			connection.connect(host, PORT);
		}
		
		private function registerConnectionEvents():void{
			connection.addEventListener(DataEvent.DATA, receive);
         	connection.addEventListener(Event.CLOSE, close);
            connection.addEventListener(IOErrorEvent.IO_ERROR, ioError);
            connection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityError);
			connection.addEventListener(CommunicationEvent.CONNECTED, connected);
			connection.addEventListener(CommunicationEvent.TIMEOUT, timeout);
		}
		
		private function unregisterConnectionEvents():void{
			connection.removeEventListener(DataEvent.DATA, receive);
         	connection.removeEventListener(Event.CLOSE, close);
            connection.removeEventListener(IOErrorEvent.IO_ERROR, ioError);
            // not removed because the socketconnection will throw a security error if no connection is available
//          connection.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, securityError); 

			connection.removeEventListener(CommunicationEvent.CONNECTED, connected);
			connection.removeEventListener(CommunicationEvent.TIMEOUT, timeout);
		}

		/**
		 * Create a message and push it to the queue of unconfirmed messages.
		 * The first message of the queue holds a timer which fires a message expired event.
		 * 
		 * Finally send the message via the give connection.
		 * @param requestType		a message type (e.g. ".sync", "init", etc.)
		 * @param parameters		message content 
		 */
		public function send(requestType:String, parameters:Array):IMessage {
			lockManager.lock(this);
			var message:IMessage = protocol.createMessage(requestType, parameters, messageQueue.nextMessageId);		
			messageQueue.addMessage(message, connection.getType());
			lockManager.unlock(this);
			return message;
		}
				
		private function onNextMessage(event:MessageEvent):void {
			connection.sendMessage(event.message);
		}

		private function securityError(event:SecurityErrorEvent):void {
			Debug.trace("Communication::securityError: " + event, Debug.WARNING);
		}

		private function timeout(event:CommunicationEvent = null):void {
			log("timeout " + event, Debug.CONNECTION);
			if(connection.used){
				reconnectTimer.stop();
				reconnect();
			}else{
				unregisterConnectionEvents();	
				log("timeout polltime: " + settings.pollSync, Debug.CONNECTION);
				connection = new PollConnection(this, settings.pollSync);
				registerConnectionEvents();
				connection.connect(host,PORT);
			}
		}

		private function close(event:Event):void {
			log("closeHandler: " + event, Debug.CONNECTION);
			if(!connectionClosed){
				log("closeHandler: reconnecting ..." + event, Debug.WARNING);
				reconnect();
			}
		}

		private function ioError(event:IOErrorEvent):void {
			log("ioErrorHandler: " + event, Debug.ERROR);
		}
		
		private function disconnect(event:Event):void {
			log("disconnect ... !!! DISCONNECTED !!!", Debug.ERROR);
			reconnectTimer.stop();
			removeEventListener(CommunicationEvent.DISCONNECT, disconnect);
			dispatchEvent(new CommunicationEvent(CommunicationEvent.DISCONNECT));
		}

		private function reconnect():void {	
			log("reconnect ...", Debug.ERROR);
			if(reconnectCount <= MAX_RECONNECTS){
				dispatchEvent(new CommunicationEvent(CommunicationEvent.RECONNECT));
				addEventListener(CommunicationEvent.CONNECTED, reconnected);
				messageQueue.reconnecting = true;
				reconnectTimer.start();
				reconnectCount++;
				try{
					connection.reconnect(host,PORT);
				}catch(e:Error){
					log("reconnect: error: " + e, Debug.ERROR);
				}
			}else{
				dispatchEvent(new CommunicationEvent(CommunicationEvent.DISCONNECT));
			}
		}

		private function onReconnectTimer(event:TimerEvent):void {
			log("onReconnectTimer ...", Debug.ERROR);
			reconnectCount++;
			if(reconnectCount > MAX_RECONNECTS){
				dispatchEvent(new CommunicationEvent(CommunicationEvent.DISCONNECT));
			}else{
				reconnect();
			}
		}
		
		private function onMessageExpired(event:MessageEvent):void{
			log("onMessageExpired: " + event.message.requestType, Debug.ERROR);
			if(event.message.requestType == CommunicationRequests.SOCKET){
				messageQueue.shift();
				timeout();
			}else{
				reconnect();
			}
		}
		
		private function reconnected(event:CommunicationEvent):void {
			log("reconnected ..." + event, Debug.CONNECTION);
			reconnectTimer.stop();
			reconnectCount = 0;
			dispatchEvent(new CommunicationEvent(CommunicationEvent.RECONNECTED));
		}
		
		public function connected(event:CommunicationEvent):void {
			log("connected", Debug.CONNECTION);
			dispatchEvent(new CommunicationEvent(CommunicationEvent.CONNECTED));
		}	

		private function receive(event:DataEvent):void {
			try{
				var serverMessages:Array = protocol.extractMessages(event.data);
				event.data = "";		
			}catch(a:Error){
				log("receive  " + a, Debug.ERROR);	
			}
			var firstMessageId:int 		= IMessage(serverMessages[0]).id;
			var attchedMessages:Boolean = (serverMessages.length > 1);
			
			var isUnhandledMessage:Boolean = Synchronisation.getInstance().onReceiveMessage(new SyncEvent(SyncEvent.RECEIVE, firstMessageId, attchedMessages));
			if(isUnhandledMessage){
				try{
					for(var n:int=0; n <serverMessages.length; n++){
						var serverMessage:IMessage = IMessage(serverMessages[n]);
						log("receive response: " + serverMessage.content, Debug.RECEIVE);			
						checkMessage(serverMessage);
					}
				}catch(ex:Error){
					log("receive  error: " + ex, Debug.ERROR);	
				}
			}
		}

		public function checkMessage(serverMessage:IMessage):void {
			switch(serverMessage.requestType) {
				case CommunicationRequests.ERROR:
					// dropping server errors - send sync instead ....
					send(CommunicationRequests.SYNC, []);
					break;
				case CommunicationRequests.ACK:
					var ackedMessage:IMessage = messageQueue.getLastMessage();
					log("checkMessage: " + serverMessage.parameters[0] + ":"+ ackedMessage.id, Debug.CONNECTION);
					if(serverMessage.parameters[0] == ackedMessage.id){
						switch(ackedMessage.requestType) {
							case CommunicationRequests.SYNC:
								dispatchEvent(new SyncEvent(SyncEvent.ACK, serverMessage.parameters[0]));
								break;
							case CommunicationRequests.SOCKET:
								dispatchEvent(new ServerMessageEvent(ackedMessage.requestType, serverMessage.parameters));	
								break;	
						}				
						messageQueue.shift();
					}
					else{
						var anId:int = serverMessage.parameters[0];
						try{
							messageQueue.removeMessageById(anId);
						}catch(e:Error){
							log("checkMessage: remove out of order message ack: error: " + e, Debug.ERROR);
						}
						dispatchEvent(new SyncEvent(SyncEvent.OUT_OF_ORDER_ACK, serverMessage.id));
					}
					break;
				default:
					dispatchEvent(new ServerMessageEvent(serverMessage.requestType, serverMessage.parameters));
			}
		}
		
		public function terminate():void {
			connectionClosed = true;
			unregisterConnectionEvents();
			messageQueue.close();
			connection.close();
		}
		
		private function log(aMessage:String, logMode:String = Debug.DEBUG):void{
			if(DEBUG_ENABLED || logMode == Debug.ERROR){
				Debug.trace("Communication::" +  aMessage, logMode);
			}
		}
	}
}
